package restx.apidocs.doclet;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Charsets;
import com.google.common.base.Optional;
import com.google.common.io.Files;
import com.sun.javadoc.AnnotationDesc;
import com.sun.javadoc.AnnotationDesc.ElementValuePair;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.DocErrorReporter;
import com.sun.javadoc.Doclet;
import com.sun.javadoc.LanguageVersion;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.ParamTag;
import com.sun.javadoc.RootDoc;
import com.sun.javadoc.Tag;
import com.sun.tools.doclets.standard.Standard;
import org.joda.time.DateTime;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import static java.util.Arrays.asList;
/**
* ApidocsDoclet.
*
* Extracts javadoc of RESTX endpoints to provide them in API DOCS.
*/
public class ApidocsDoclet extends Doclet {
/**
* The starting point of Javadoc render.
*
* _Javadoc spec requirement._
*
* @param rootDoc input class documents
* @return success
*/
@SuppressWarnings("UnusedDeclaration")
public static boolean start(RootDoc rootDoc) {
Path targetDir = Paths.get(Options.TARGET_DIR.getOption(rootDoc.options()).or(""));
rootDoc.printNotice("generating RESTX apidocs notes in: " + targetDir + " ...");
Path apidocsTarget = targetDir.resolve("apidocs");
if (!apidocsTarget.toFile().exists()) {
apidocsTarget.toFile().mkdirs();
}
Trace trace = Options.ENABLE_TRACE.isSet(rootDoc.options()) ?
new FileTrace(targetDir.resolve("apidoclet.trace").toFile()) :
new NoTrace()
;
trace.trace("RESTX APIDOCLET " + DateTime.now());
trace.trace("target dir : " + targetDir.toAbsolutePath());
trace.trace("current dir: " + Paths.get("").toAbsolutePath());
ObjectMapper mapper = new ObjectMapper();
for (ClassDoc classDoc : rootDoc.classes()) {
ApiEntryNotes entryNotes = new ApiEntryNotes().setName(classDoc.qualifiedName());
for (MethodDoc methodDoc : classDoc.methods()) {
for (AnnotationDesc annotationDesc : methodDoc.annotations()) {
if (annotationDesc.annotationType().qualifiedName().startsWith("restx.annotations.")) {
Optional<Object> value = getAnnotationParamValue(annotationDesc.elementValues(), "value");
if (value.isPresent()) {
trace.trace(classDoc.name() + " > " + methodDoc.qualifiedName() + " > " + annotationDesc.annotationType().name());
trace.trace(asList(annotationDesc.elementValues()).toString());
trace.trace(methodDoc.commentText());
ApiOperationNotes operation = new ApiOperationNotes()
.setHttpMethod(annotationDesc.annotationType().name())
.setPath(String.valueOf(value.get()))
.setNotes(methodDoc.commentText());
for (ParamTag paramTag : methodDoc.paramTags()) {
trace.trace("\t" + paramTag.parameterName() + " > " + paramTag.parameterComment());
operation.getParameters().add(
new ApiParameterNotes()
.setName(paramTag.parameterName())
.setNotes(paramTag.parameterComment()));
}
for (Tag aReturn : methodDoc.tags("return")) {
trace.trace("\t" + aReturn.name() + " > " + aReturn.text());
operation.getParameters().add(
new ApiParameterNotes()
.setName("response")
.setNotes(aReturn.text()));
}
entryNotes.getOperations().add(operation);
}
}
}
}
if (!entryNotes.getOperations().isEmpty()) {
Path doc = apidocsTarget.resolve(classDoc.qualifiedName() + ".notes.json");
rootDoc.printNotice("generating RESTX API entry notes for " + classDoc.qualifiedName() + " ...");
trace.trace("generating notes in " + doc.toAbsolutePath());
try {
mapper.writeValue(doc.toFile(), entryNotes);
} catch (IOException e) {
trace.trace("can't write to api doc file " + doc.toFile() + ": " + e);
rootDoc.printError("can't write to api doc file " + doc.toFile() + ": " + e);
}
} else {
trace.trace("no operations found on " + entryNotes.getName());
}
}
if (Options.DISABLE_STANDARD_DOCLET.isSet(rootDoc.options())) {
return true;
}
return Standard.start(rootDoc);
}
private static Optional<Object> getAnnotationParamValue(ElementValuePair[] elementValuePairs, String paramName) {
for (ElementValuePair pair : elementValuePairs) {
if (pair.element().name().equals(paramName)) {
return Optional.of(pair.value().value());
}
}
return Optional.absent();
}
/**
* Sets the language version to Java 5.
*
* _Javadoc spec requirement._
*
* @return language version number
*/
@SuppressWarnings("UnusedDeclaration")
public static LanguageVersion languageVersion() {
return LanguageVersion.JAVA_1_5;
}
/**
* Sets the option length to the standard Javadoc option length.
*
* _Javadoc spec requirement._
*
* @param option input option
* @return length of required parameters
*/
@SuppressWarnings("UnusedDeclaration")
public static int optionLength(String option) {
for (Options opt : Options.values()) {
if (opt.getOptionName().equalsIgnoreCase(option)) {
return opt.getOptionLength();
}
}
return Standard.optionLength(option);
}
/**
* Processes the input options by delegating to the standard handler.
*
* _Javadoc spec requirement._
*
* @param options input option array
* @param errorReporter error handling
* @return success
*/
@SuppressWarnings("UnusedDeclaration")
public static boolean validOptions(String[][] options, DocErrorReporter errorReporter) {
return Standard.validOptions(options, errorReporter);
}
static enum Options {
DISABLE_STANDARD_DOCLET("-disable-standard-doclet", 1),
TARGET_DIR("-restx-target-dir", 2),
ENABLE_TRACE("-restx-enable-trace", 1);
private final String optionName;
private final int optionLength;
Options(String name, int optionLength) {
this.optionName = name;
this.optionLength = optionLength;
}
public String getOptionName() {
return optionName;
}
public int getOptionLength() {
return optionLength;
}
public boolean isSet(String[][] options) {
for (String[] option : options) {
if (options.length > 0 && optionName.equals(option[0])) {
return true;
}
}
return false;
}
public Optional<String> getOption(String[][] options) {
for (String[] option : options) {
if (options.length > 1 && optionName.equals(option[0])) {
return Optional.of(option[1]);
}
}
return Optional.absent();
}
}
private static interface Trace {
public void trace(String msg);
}
private static class FileTrace implements Trace {
private final File traceFile;
private FileTrace(File traceFile) {
this.traceFile = traceFile;
}
@Override
public void trace(String msg) {
try {
Files.append(msg + "\n", traceFile, Charsets.UTF_8);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
private static class NoTrace implements Trace {
@Override
public void trace(String msg) {
}
}
}